// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection.Metadata;
using Microsoft.CodeAnalysis;
using ILLink.RoslynAnalyzer.TrimAnalysis;
using ILLink.Shared.TypeSystemProxy;
using System.Collections.Immutable;

namespace ILLink.Shared.TrimAnalysis
{
    internal struct TypeNameResolver
    {
        readonly Compilation _compilation;

        static readonly TypeNameParseOptions s_typeNameParseOptions = new() { MaxNodes = int.MaxValue };

        public TypeNameResolver(Compilation compilation)
        {
            _compilation = compilation;
        }

        public bool TryResolveTypeName(
            string typeNameString,
            in DiagnosticContext diagnosticContext,
            out ITypeSymbol? type,
            bool needsAssemblyName)
        {
            type = null;
            if (!TypeName.TryParse(typeNameString.AsSpan(), out TypeName? typeName, s_typeNameParseOptions))
                return false;

            if (needsAssemblyName && !IsFullyQualified(typeName))
            {
                diagnosticContext.AddDiagnostic(DiagnosticId.TypeNameIsNotAssemblyQualified, typeNameString);
                return false;
            }

            type = ResolveTypeName(_compilation.Assembly, typeName);
            return type != null;

            static bool IsFullyQualified(TypeName typeName)
            {
                if (typeName.AssemblyName is null)
                    return false;

                if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef)
                    return IsFullyQualified(typeName.GetElementType());

                if (typeName.IsConstructedGenericType)
                {
                    foreach (var arg in typeName.GetGenericArguments())
                    {
                        if (!IsFullyQualified(arg))
                            return false;
                    }
                }

                return true;
            }
        }

        ITypeSymbol? ResolveTypeName(IAssemblySymbol assembly, TypeName typeName)
        {
            if (typeName.IsSimple)
                return GetSimpleType(assembly, typeName);

            if (typeName.IsConstructedGenericType)
                return GetGenericType(assembly, typeName);

            if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef)
            {
                if (ResolveTypeName(assembly, typeName.GetElementType()) is not ITypeSymbol type)
                    return null;

                if (typeName.IsArray)
                    return typeName.IsSZArray ? _compilation.CreateArrayTypeSymbol(type) : _compilation.CreateArrayTypeSymbol(type, typeName.GetArrayRank());

                // Roslyn doesn't have a representation for byref types
                // (the byrefness is considered part of the symbol, not its type)
                if (typeName.IsByRef)
                    return null;

                if (typeName.IsPointer)
                    return _compilation.CreatePointerTypeSymbol(type);

                Debug.Fail("Unreachable");
                return null;
            }

            return null;
        }

        private ITypeSymbol? GetSimpleType(IAssemblySymbol assembly, TypeName typeName)
        {
            IAssemblySymbol module = assembly;
            if (typeName.AssemblyName is AssemblyNameInfo assemblyName)
            {
                if (ResolveAssembly(assemblyName) is not IAssemblySymbol resolvedAssembly)
                    return null;
                module = resolvedAssembly;
            }

            if (GetSimpleTypeFromModule(typeName, module) is ITypeSymbol type)
                return type;

            // The analyzer doesn't see the core library, so can't fall back to lookup up types in corelib.
            return null;
        }

        private static ITypeSymbol? GetSimpleTypeFromModule(TypeName typeName, IAssemblySymbol module)
        {
            string fullName = TypeName.Unescape(typeName.FullName);
            return module.GetTypeByMetadataName(fullName);
        }

        private ITypeSymbol? GetGenericType(IAssemblySymbol assembly, TypeName typeName)
        {
            if (ResolveTypeName(assembly, typeName.GetGenericTypeDefinition()) is not INamedTypeSymbol typeDefinition)
                return null;

            ImmutableArray<TypeName> typeArguments = typeName.GetGenericArguments();
            ITypeSymbol[] instantiation = new ITypeSymbol[typeArguments.Length];
            for (int i = 0; i < typeArguments.Length; i++)
            {
                if (ResolveTypeName(assembly, typeArguments[i]) is not ITypeSymbol type)
                    return null;
                instantiation[i] = type;
            }
            return typeDefinition.Construct(instantiation);
        }

        IAssemblySymbol? ResolveAssembly(AssemblyNameInfo? assemblyName)
        {
            if (assemblyName is null)
                return null;

            if (_compilation.Assembly.Name == assemblyName.Name)
                return _compilation.Assembly;

            foreach (var metadataReference in _compilation.References)
            {
                if (_compilation.GetAssemblyOrModuleSymbol(metadataReference) is not IAssemblySymbol asmSym)
                    continue;
                if (asmSym.Name == assemblyName.Name)
                    return asmSym;
            }
            return null;
        }
    }
}
